# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: jaewon

import datetime
import logging

from tornado.ioloop import PeriodicCallback

from coin.base.param_util import to_list
from coin.base.timestamp import get_timestamp
from coin.strategy.mm.subscription import SubscriptionRequest


def need_check_limit_price(FeedSubReq, fs_config):
  if FeedSubReq in [
      SubscriptionRequest('Futures', 'Okex', 'v1'),
      SubscriptionRequest('Futures', 'Okex', 'v3'),
      SubscriptionRequest('Futures', 'Okex', None)
  ]:
    if fs_config is not None:
      return fs_config.check_limit_price
    else:
      return False
  else:
    return None


class BaseFeedSubsystem(object):
  FeedSubReq = None
  FeedSubscriber = None
  FlowBookBuilder = None

  _cooltime_reconnect = datetime.timedelta(seconds=3)
  _start_immediately = True
  _use_feed_checker = True

  def __init__(self,
               products,
               on_book_init_callback,
               worker_id=1,
               feed_checker_config=None,
               fs_config=None,
               logger=None):
    assert isinstance(products, list)

    self._logger = logger or logging.getLogger(__name__)

    self._products = products
    self._on_book_init_callback = on_book_init_callback

    self._worker_id = worker_id
    self._ioloop = None
    self._queue = None
    self._flow_sub = None

    self._subscriber = None
    self._book_builder = None
    self._last_book_ts = None
    self._available = False

    # Binance?
    self._ws_started = False
    self._fs_config = fs_config

    self._feed_checker_config = feed_checker_config
    self._feed_monitor_periodic_callback = None
    self._check_limit_price = need_check_limit_price(self.FeedSubReq, fs_config)
    self._price_limit_error_callback = None

  @property
  def available(self):
    return self._available

  def set_price_limit_error_callback(self, callback):
    raise NotImplementedError(self.FeedSubReq)

  def _on_book(self, book):
    self._last_book_ts = book.timestamp

  def _on_disconnect(self):
    self._available = False
    self._logger.error('Disconnected. Restarting in %s.', self._cooltime_reconnect)
    self._ioloop.add_timeout(self._cooltime_reconnect, self._init_subscriber)

  def _monitor_feed(self):
    if self._last_book_ts is None:
      return

    diff = get_timestamp() - self._last_book_ts
    self._logger.info('%s last feed: %d ms ago' % (self.FeedSubReq, diff / 10**6))

    if hasattr(self._subscriber, "BookFeedDisconnectWindowInNs"):
      conn_window = self._subscriber.BookFeedDisconnectWindowInNs
    else:
      conn_window = 2 * 60 * 1e9  # 2 mins

    if diff < conn_window:
      return
    else:
      self._logger.error('Monitor feed detect discontinuity. thres: %d(ms)', (conn_window * 1e-6))

    try:
      if self._subscriber is not None:
        self._subscriber.stop()
    except Exception:
      self._logger.exception('Error stopping subscriber')
    self._on_disconnect()

  def _monitor_feed_checker(self):
    assert self._feed_checker is not None
    if self._last_book_ts is None:
      return

    timestamp = get_timestamp()
    state = self._feed_checker.check_state(timestamp=timestamp)
    if state.ready:
      self._available = True
      return
    if not state.invalid:
      return

    self._feed_checker.print_status(timestamp)
    self._feed_checker.restart(timestamp=timestamp)
    self._logger.error('Feed checker result is invalid, reconnect...')

    try:
      if self._subscriber is not None:
        self._subscriber.stop()
    except Exception:
      self._logger.exception('Error stopping subscriber')
    self._on_disconnect()

  def _monitor_limit_price_checker(self):
    state = self._limit_price_checker.check_state()
    if not state:
      self._limit_price_checker.print_status()

    if self._price_limit_error_callback is not None:
      self._price_limit_error_callback(state)

  def _on_ws_record(self, native_symbols, record, queue_data, topic_data):
    if self._ws_started:
      return
    self._ws_started = True

  def _get_subscription(self):
    return [product.native_symbol for product in self._products]

  def _create_book_builder(self):
    if self._check_limit_price is not None:
      self._book_builder = self.FlowBookBuilder(self._flow_sub,
                                                self._worker_id,
                                                self._feed_checker_config,
                                                check_limit_price=self._check_limit_price)
    else:
      self._book_builder = self.FlowBookBuilder(self._flow_sub,
                                                self._worker_id,
                                                self._feed_checker_config)
    self._book_builder.set_live(True)

  def _init_subscriber(self, subscriber_group_id='a'):
    if self._subscriber is not None:
      self._subscriber.stop()
      self._logger.info('Restarting subscriber...')

    if self.FeedSubReq == SubscriptionRequest('Spot', 'Uniswap', 'v3-arbitrum'):
      self._subscriber = self.FeedSubscriber(self._queue,
                                             subscriber_group_id,
                                             self._worker_id,
                                             self._get_subscription(),
                                             close_callback=self._on_disconnect,
                                             provider=self.ProviderUrl,
                                             blockchain_name="arbitrum")
    else:
      self._subscriber = self.FeedSubscriber(self._queue,
                                             subscriber_group_id,
                                             self._worker_id,
                                             self._get_subscription(),
                                             close_callback=self._on_disconnect)
    self._subscriber.start(self._ioloop)
    '''
    # From binance
    self._ws_started = False
    self._flow_sub.subscribe(
        'binance_ws_depth20_a%d' % self._worker_id,
        functools.partial(self._on_ws_record, binance_native_symbols))
    '''

    # Intialize book builder
    self._create_book_builder()
    for product in self._products:
      self._book_builder.subscribe(product, self._on_book)
    self._last_book_ts = get_timestamp()

    # Callback
    self._on_book_init_callback(self.FeedSubReq, self._book_builder)

    # Feed checker
    if self._use_feed_checker:
      self._feed_checker = self._book_builder._feed_handler.feed_checker
      if self._feed_checker:
        self._feed_checker.restart(timestamp=self._last_book_ts)

    if self._start_immediately:
      self._available = True
    else:
      self._available = False

  def start(self, ioloop, queue, flow_sub):
    self._ioloop = ioloop
    self._queue = queue
    self._flow_sub = flow_sub

    # Init feed subscriber
    self._init_subscriber()

    # Setup feed monitor
    self._feed_monitor_periodic_callback = PeriodicCallback(self._monitor_feed, 60 * 1000)
    self._feed_monitor_periodic_callback.start()

    # Setup feed checker monitor
    if self._feed_checker_config is not None:
      self._feed_checker_monitor_periodic_callback = PeriodicCallback(self._monitor_feed_checker,
                                                                      1 * 1000)
      self._feed_checker_monitor_periodic_callback.start()

    # Setup limit price checker monitor
    if self._check_limit_price:
      self._limit_price_checker = \
          self._book_builder._feed_handler.limit_price_checker
      self._limit_price_checker_periodic_callback = PeriodicCallback(
          self._monitor_limit_price_checker, 5 * 1000)
      self._limit_price_checker_periodic_callback.start()

  def restart(self):
    try:
      if self._subscriber is not None:
        self._subscriber.stop()
    except Exception:
      self._logger.exception('Error stopping subscriber')
    self._on_disconnect()
